/*
 *  Copyright (C) 2010 Matthias Buch-Kromann <mbk.isv@cbs.dk>
 * 
 *  This file is part of the IncrementalParser package.
 *  
 *  The IncrementalParser program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 * 
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 * 
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.osdtsystem.incparser.graphs;

import gnu.trove.TIntIterator;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import gnu.trove.TIntHashSet;
import java.util.Arrays;
import java.util.Map.Entry;

/**
 *
 * @author Matthias Buch-Kromann <mbk.isv@cbs.dk>
 */
public class BaseGraphGrowable extends AbstractBaseGraph {
    // Local graphs
    int nodeCount = 0;
    ArrayList<AbstractLocalGraph> localGraphs = new ArrayList<AbstractLocalGraph>();
    ArrayList<String> layerNames = new ArrayList<String>();

    // Features
    int intFeatureCount = AbstractGraph.INITIALINTFEATURECOUNT;
    int objectFeatureCount = AbstractGraph.INITIALOBJECTFEATURECOUNT;
    ArrayList<int[]> intFeatures = new ArrayList<int[]>();
    ArrayList<Object[]> objectFeatures = new ArrayList<Object[]>();
    ArrayList<String> intFeatureNames = new ArrayList<String>();
    ArrayList<String> objectFeatureNames = new ArrayList<String>();

    // Scores
    float score = 0;
    ArrayList<GraphScorer> scorers = new ArrayList<GraphScorer>();
    ArrayList<float[]> scores = new ArrayList<float[]>();
    ArrayList<float[]> bestScores = new ArrayList<float[]>();
    TIntHashSet outdatedScores = new TIntHashSet();

    // Listeners and notification
    boolean isNotificationOn = true;
    HashMap<Integer,List<GraphListener>> graphListeners
            = new HashMap<Integer,List<GraphListener>>();
    HashMap<Integer,List<GraphListener>> intFeatureListeners
            = new HashMap<Integer,List<GraphListener>>();
    HashMap<Integer,List<GraphListener>> objectFeatureListeners
            = new HashMap<Integer,List<GraphListener>>();

    // Defaults
    float[] defaultScores = new float[0];
    float[] defaultBestScores = new float[0];

    // Constructors and cloning
    public BaseGraphGrowable(String... layerNames) {
        for (String layerName : layerNames)
            addLayer(layerName);
    }

    public BaseGraphGrowable() {
        this("nodes");
    }

    static HashMap<Integer,List<GraphListener>> cloneGraphListenerMap(HashMap<Integer,List<GraphListener>> map) {
        HashMap<Integer,List<GraphListener>> clone = (HashMap<Integer, List<GraphListener>>) map.clone();
        for (Entry<Integer,List<GraphListener>> entry : map.entrySet()) {
            List<GraphListener> list = entry.getValue();
            entry.setValue(new ArrayList<GraphListener>(list.size()));
        }
        return clone;
    }

    @Override
    public BaseGraphGrowable clone() {
        try {
            BaseGraphGrowable clone = (BaseGraphGrowable) super.clone();
            
            // Simple cloning without element cloning
            clone.intFeatureNames = (ArrayList<String>) intFeatureNames.clone();
            clone.objectFeatureNames = (ArrayList<String>) objectFeatureNames.clone();
            clone.scorers = (ArrayList<GraphScorer>) scorers.clone();
            clone.outdatedScores = (TIntHashSet) outdatedScores.clone();

            // HashMap cloning with element cloning
            clone.graphListeners = cloneGraphListenerMap(graphListeners);
            clone.intFeatureListeners = cloneGraphListenerMap(intFeatureListeners);
            clone.objectFeatureListeners = cloneGraphListenerMap(objectFeatureListeners);

            // Clone intFeatures
            clone.intFeatures = (ArrayList<int[]>) intFeatures.clone();
            for (int i = intFeatures.size() - 1; i >= 0; --i)
                clone.intFeatures.set(i, intFeatures.get(i).clone());

            // Clone objectFeatures
            clone.objectFeatures = (ArrayList<Object[]>) objectFeatures.clone();
            for (int i = objectFeatures.size() - 1; i >= 0; --i)
                clone.objectFeatures.set(i, objectFeatures.get(i).clone());

            // Clone scores
            clone.scores = (ArrayList<float[]>) scores.clone();
            for (int i = scores.size() - 1; i >= 0; --i)
                clone.scores.set(i, scores.get(i).clone());

            // Clone best scores
            clone.bestScores = (ArrayList<float[]>) bestScores.clone();
            for (int i = bestScores.size() - 1; i >= 0; --i)
                clone.bestScores.set(i, bestScores.get(i).clone());

            // Return
            return clone;
        } catch (CloneNotSupportedException ex) {
            throw new InternalError("Base graph cloning failed");
        }

    }

    public void trimToSize() {
        localGraphs.trimToSize();
        intFeatures.trimToSize();
        objectFeatures.trimToSize();
        intFeatureNames.trimToSize();
        objectFeatureNames.trimToSize();
        scores.trimToSize();
        bestScores.trimToSize();
        outdatedScores.trimToSize();
        scorers.trimToSize();
    }

    // Local graphs
    public int addLayer(String layerName) {
        // Update node graphs (expensive!)
        if (nodes() > 0) {
            int oldLayerCount = layers();
            ArrayList<AbstractLocalGraph> newLocalGraphs = new ArrayList<AbstractLocalGraph>(nodes() * (oldLayerCount + 1));
            for (int node = 0; node < nodes(); ++node) {
                for (int layer = 0; layer < oldLayerCount; ++layer)
                    newLocalGraphs.add(localGraphs.get(localGraphIndex(node, layer)));
                newLocalGraphs.add(AbstractLocalGraph.LOCALGRAPHTEMPLATE.clone());
            }
            localGraphs = newLocalGraphs;
        }

        // Insert new layer and return layer id
        layerNames.add(layerName);
        return layerNames.size() - 1;
    }

    public int layers() {
        return layerNames.size();
    }

    public int nodes() {
        return nodeCount;
    }

    public int nodeLayer(int node) {
        return intFeature(node, AbstractGraph.INTFEATURE_LAYER);
    }

    public LocalGraph localGraph(int node, int layer) {
        return _localGraphRW(node, layer);
    }

    public AbstractLocalGraph _localGraphRW(int node, int layer) {
        if (node < 0 || node >= nodes())
            throw new IndexOutOfBoundsException("Illegal node index " + node);
        return localGraphs.get(localGraphIndex(node, layer));
    }

    int localGraphIndex(int node, int layer) {
        return node * layers() + layer;
    }

    // Features
    public int intFeatures() {
        return intFeatureCount;
    }

    public int objectFeatures() {
        return objectFeatureCount;
    }

    public int intFeature(int node, int feature) {
        if (node < 0 || node >= nodes())
            throw new IllegalArgumentException("Illegal node index " + node);
        if (feature < 0 || feature >= intFeatures())
            throw new IllegalArgumentException("Illegal int feature index " + feature);
        int[] nodeFeatures = intFeatures.get(node);
        return nodeFeatures != null ? nodeFeatures[feature] : Graph.NULLINTFEATURE;
    }

    public Object objectFeature(int node, int feature) {
        if (node < 0 || node >= nodes())
            throw new IllegalArgumentException("Illegal node index " + node);
        if (feature < 0 || feature >= objectFeatures())
            throw new IllegalArgumentException("Illegal object feature index " + feature);
        Object[] nodeFeatures = objectFeatures.get(node);
        return nodeFeatures != null ? nodeFeatures[feature] : Graph.NULLOBJECTFEATURE;
    }

    public void setIntFeature(int node, int feature, int value) {
        if (node < 0 || node >= nodes())
            throw new IllegalArgumentException("Illegal node index " + node);
        if (feature < 0 || feature >= intFeatures())
            throw new IllegalArgumentException("Illegal int feature index " + feature);
        int[] nodeFeatures = intFeatures.get(node);
        if (nodeFeatures == null)
            intFeatures.set(node, nodeFeatures = new int[intFeatures()]);
        nodeFeatures[feature] = value;
    }

    public void setObjectFeature(int node, int feature, Object value) {
        if (node < 0 || node >= nodes())
            throw new IllegalArgumentException("Illegal node index " + node);
        if (feature < 0 || feature >= objectFeatures())
            throw new IllegalArgumentException("Illegal object feature index " + feature);
        Object[] nodeFeatures = objectFeatures.get(node);
        if (nodeFeatures == null)
            objectFeatures.set(node, nodeFeatures = new Object[objectFeatures()]);
        nodeFeatures[feature] = value;
    }

    public int addIntFeature(String name) {
        // Increase int feature count
        intFeatureNames.add(name);
        ++intFeatureCount;

        // Reallocate feature arrays for all existing nodes (expensive!)
        for (int node = nodes() - 1; node >= 0; --node) {
            int[] nodeFeatures = intFeatures.get(node);
            if (nodeFeatures != null) {
                int[] newNodeFeatures = Arrays.copyOf(nodeFeatures, intFeatureCount);
                newNodeFeatures[intFeatureCount - 1] = NULLINTFEATURE;
                intFeatures.set(node, newNodeFeatures);
            }
        }

        // Return new feature index
        return intFeatureCount - 1;
    }

    public int addObjectFeature(String name) {
        // Increase object feature count
        objectFeatureNames.add(name);
        ++objectFeatureCount;

        // Reallocate feature arrays for all existing nodes (expensive!)
        for (int node = nodes() - 1; node >= 0; --node) {
            Object[] nodeFeatures = objectFeatures.get(node);
            if (nodeFeatures != null) {
                Object[] newNodeFeatures = Arrays.copyOf(nodeFeatures, objectFeatureCount);
                newNodeFeatures[objectFeatureCount - 1] = NULLOBJECTFEATURE;
                objectFeatures.set(node, newNodeFeatures);
            }
        }

        // Return new feature index
        return objectFeatureCount - 1;
    }


    // Graph operations
    public BaseGraph writeToBaseGraph() {
        return this;
    }

    AbstractBaseGraph _writeToBaseGraphRW() {
        return this;
    }

    AbstractBaseGraph _baseGraph() {
        return this;
    }

    public GraphOverlayAddNode makeOverlayAddNode(int layer) {
        return new GraphOverlayAddNode(this, layer);
    }

    public GraphOverlayAddEdge makeOverlayAddEdge(int layer, int innode, int outnode, int type) {
        return new GraphOverlayAddEdge(this, layer, innode, outnode, type);
    }

    public GraphOverlayRemoveEdge makeOverlayRemoveEdge(int layer, int innode, int outnode, int type) {
        return new GraphOverlayRemoveEdge(this, layer, innode, outnode, type);
    }

    public GraphOverlayMoveEdge makeOverlayMoveEdge(int layer, int innode, int oldoutnode, int oldtype, int newoutnode, int newtype) {
        return new GraphOverlayMoveEdge(this, layer, innode, oldoutnode, oldtype, newoutnode, newtype);
    }


    // Base graph operations
    public int addNode(int layer) {
        if (layer < 0 || layer >= layers())
            throw new IllegalArgumentException("Illegal layer " + layer);
        int node = nodeCount++;
        for (int i = layers() - 1; i >= 0; --i)
            localGraphs.add(AbstractLocalGraph.LOCALGRAPHTEMPLATE.clone());
        intFeatures.add(null);
        objectFeatures.add(null);
        float[] nodescores = new float[scorers()];
        Arrays.fill(nodescores, Float.NaN);
        scores.add(nodescores);
        bestScores.add(nodescores.clone());
        for (int scorer = scorers.size() - 1; scorer >= 0; --scorer)
            outdatedScores.add(Helper.pack(node, scorer));
        return node;
    }


    public void addEdge(int layer, int innode, int outnode, int type) {
        if (layer < 0 || layer >= layers())
            throw new IllegalArgumentException("Illegal layer " + layer);
        if (innode < 0 || innode >= nodes())
            throw new IllegalArgumentException("Illegal innode " + innode);
        if (outnode < 0 || outnode >= nodes())
            throw new IllegalArgumentException("Illegal outnode " + outnode);
        localGraphs.get(localGraphIndex(innode, layer)).addInEdge(Helper.pack(outnode, type));
        localGraphs.get(localGraphIndex(outnode, layer)).addOutEdge(Helper.pack(innode, type));
    }

    public void removeEdge(int layer, int innode, int outnode, int type) {
        if (layer < 0 || layer >= layers())
            throw new IllegalArgumentException("Illegal layer " + layer);
        if (innode < 0 || innode >= nodes())
            throw new IllegalArgumentException("Illegal innode " + innode);
        if (outnode < 0 || outnode >= nodes())
            throw new IllegalArgumentException("Illegal outnode " + outnode);
        localGraphs.get(localGraphIndex(innode, layer)).removeInEdge(Helper.pack(outnode, type));
        localGraphs.get(localGraphIndex(outnode, layer)).removeOutEdge(Helper.pack(innode, type));
    }

    public void moveEdge(int layer, int innode, int oldoutnode, int oldtype, int newoutnode, int newtype) {
        if (layer < 0 || layer >= layers())
            throw new IllegalArgumentException("Illegal layer " + layer);
        if (innode < 0 || innode >= nodes())
            throw new IllegalArgumentException("Illegal innode " + innode);
        if (oldoutnode < 0 || oldoutnode >= nodes())
            throw new IllegalArgumentException("Illegal old outnode " + oldoutnode);
        if (newoutnode < 0 || newoutnode >= nodes())
            throw new IllegalArgumentException("Illegal new outnode " + newoutnode);
        AbstractLocalGraph inNodeGraph = localGraphs.get(innode);
        inNodeGraph.removeInEdge(Helper.pack(oldoutnode, oldtype));
        inNodeGraph.addInEdge(Helper.pack(newoutnode, newtype));
        localGraphs.get(oldoutnode).removeOutEdge(Helper.pack(innode, oldtype));
        localGraphs.get(newoutnode).addOutEdge(Helper.pack(innode, newtype));
    }

    public void removeEdges(int layer) {
        if (layer < 0 || layer >= layers())
            throw new IllegalArgumentException("Illegal layer " + layer);
        for (int node = nodes() - 1; node >= 0; --node)
            localGraphs.get(localGraphIndex(node, layer)).clear();
    }

    public void clearEdges() {
        for (int layer = 0; layer < layers(); ++layer)
            removeEdges(layer);
    }
    
    public void clearNodes() {
        // Local graphs
        nodeCount = 0;
        localGraphs.clear();

        // Features
        intFeatures.clear();
        objectFeatures.clear();

        // Scores
        scores.clear();
        bestScores.clear();
        outdatedScores.clear();

        // Listeners and notification
        isNotificationOn = true;
    }

    // Scorers
    public int scorers() {
        return scorers.size();
    }

    public float score() {
        // Update all outdated scores
        TIntIterator iterator = outdatedScores.iterator();
        while (iterator.hasNext()) {
            int packed = iterator.next();
            _updateLocalScore(Helper.nodePart(packed), Helper.scorerPart(packed));
            iterator.remove();
        }

        // Return updated score
        return score;
    }

    public float localScore(int node, int scorer) {
        if (outdatedScores.contains(Helper.pack(node, scorer)))
            return _updateLocalScore(node, scorer);
        return scores.get(node)[scorer];
    }

    float _updateLocalScore(int node, int scorer) {
        float[] nodeScores = scores.get(node);
        float oldvalue = nodeScores[scorer];
        float value = scorer(scorer).score(this, node);
        nodeScores[scorer] = value;
        score += (oldvalue == Float.NaN) ? value : value - oldvalue;
        return value;
    }

    public float localScoreBest(int node, int scorer) {
        return bestScores.get(node)[scorer];
    }

    public float localScoreParent(int node, int scorer) {
        return localScore(node, scorer);
    }

    public float localScoreBase(int node, int scorer) {
        return localScore(node, scorer);
    }

    public void _setLocalScore(int node, int scorer, float value) {
        scores.get(node)[scorer] = value;
    }

    public GraphScorer scorer(int scorer) {
        return scorers.get(scorer);
    }


    public void _updateLocalScoreBest(int node, int scorer, float localScore) {
        float[] nodeBestScores = bestScores.get(node);
        if (nodeBestScores[scorer] < localScore)
            nodeBestScores[scorer] = localScore;
    }

    // Listeners
    public void addGraphListener(GraphListener listener, int[] layers) {
        for (int layer : layers) {
            if (! graphListeners.containsKey(layer))
                graphListeners.put(layer, new ArrayList<GraphListener>());
            List<GraphListener> list = graphListeners.get(layer);
            if (! list.contains(listener))
                list.add(listener);
        }
    }

    public void addObjectFeatureListener(GraphListener listener, int[] features) {
        for (int feature : features) {
            if (! objectFeatureListeners.containsKey(feature))
                objectFeatureListeners.put(feature, new ArrayList<GraphListener>());
            List<GraphListener> list = objectFeatureListeners.get(feature);
            if (! list.contains(listener))
                list.add(listener);
        }
    }

    public void addIntFeatureListener(GraphListener listener, int[] features) {
        for (int feature : features) {
            if (! intFeatureListeners.containsKey(feature))
                intFeatureListeners.put(feature, new ArrayList<GraphListener>());
            List<GraphListener> list = intFeatureListeners.get(feature);
            if (! list.contains(listener))
                list.add(listener);
        }
    }

    public int addGraphScorer(GraphScorer scorer) {
        if (! scorers.contains(scorer)) {
            scorers.add(scorer);
            
            // Create new default scores
            defaultScores = new float[scorers()];
            Arrays.fill(defaultScores, 0);
            defaultBestScores = defaultScores.clone();
            Arrays.fill(defaultBestScores, Float.NEGATIVE_INFINITY);
            
            // Update nodes (expensive!)
            int scorerCount = scorers();
            float[] newBestScores;
            for (int node = nodes() - 1; node >= 0; --node) {
                scores.set(node, Arrays.copyOf(scores.get(node), scorerCount));
                newBestScores = Arrays.copyOf(bestScores.get(node), scorerCount);
                newBestScores[scorerCount - 1] = Float.NEGATIVE_INFINITY;
                bestScores.set(node, newBestScores);
                outdateScore(node, scorerCount - 1);
            }
        }
        return scorers() - 1;
    }

    public void outdateScore(int node, int scorer) {
        outdatedScores.add(Helper.pack(node, scorer));
    }


    // Notification
    public void _notifyIntFeatureChanged(Graph graph, int node, int feature,
            int value, int oldvalue) {
        if (_isNotificationOn() && intFeatureListeners.containsKey(feature)) {
            for (GraphListener listener : intFeatureListeners.get(feature))
                listener.intFeatureChanged(graph, node, feature, value, oldvalue);
        }
    }

    public void _notifyObjectFeatureChanged(Graph graph, int node, int feature,
            Object value, Object oldvalue) {
        if (_isNotificationOn() && objectFeatureListeners.containsKey(feature)) {
            for (GraphListener listener : objectFeatureListeners.get(feature))
                listener.objectFeatureChanged(graph, node, feature, value, oldvalue);
        }
    }

    public void _notifyEdgeAdded(Graph graph, int layer, int innode, int outnode, int type) {
        if (_isNotificationOn() && graphListeners.containsKey(layer)) {
            for (GraphListener listener : graphListeners.get(layer))
                listener.edgeAdded(graph, layer, innode, outnode, type);
        }
    }

    public void _notifyEdgeMoved(Graph graph, int layer, int innode, int oldoutnode, int oldtype, int outnode, int type) {
        if (_isNotificationOn() && graphListeners.containsKey(layer)) {
            for (GraphListener listener : graphListeners.get(layer))
                listener.edgeMoved(graph, layer, innode, oldoutnode, oldtype, outnode, type);
        }
    }

    public void _notifyEdgeTypeChanged(Graph graph, int layer, int innode, int outnode, int oldtype, int newtype) {
        if (_isNotificationOn() && graphListeners.containsKey(layer)) {
            for (GraphListener listener : graphListeners.get(layer))
                listener.edgeTypeChanged(graph, layer, innode, outnode, oldtype, newtype);
        }
    }

    public void _notifyEdgeRemoved(Graph graph, int layer, int innode, int outnode, int type) {
        if (_isNotificationOn() && graphListeners.containsKey(layer)) {
            for (GraphListener listener : graphListeners.get(layer))
                listener.edgeRemoved(graph, layer, innode, outnode, type);
        }
    }

    public void _notifyNodeAdded(Graph graph, int layer, int node) {
        if (_isNotificationOn() && graphListeners.containsKey(layer)) {
            for (GraphListener listener : graphListeners.get(layer))
                listener.nodeAdded(graph, node);
        }
    }

    public void _notifyOn() {
        isNotificationOn = true;
    }

    public void _notifyOff() {
        isNotificationOn = false;
    }

    public boolean _isNotificationOn() {
        return isNotificationOn;
    }
}
